<?php
namespace hema\wechat;

use app\common\model\Applet;
use app\common\model\Setting;
use think\facade\Cache;

/**
 * 微信支付
 */
class Pay
{
    private $config; // 微信支付参数

    /**
     * 构造方法
     */
    public function __construct(array $config = [])
    {
        $this->config = $config;
    }

    /**
     * 统一下单API
     * 参数 $order_no=订单号 $openid=微信用户ID, $total_fee=支付金额, $trade_type=支付类型（JSAPI,NATIVE）,$attach=订单描述
     */
    public function unifiedorder($order_no, $total_fee, string $notify_url, string $openid='',$attach = '订单支付')
    {
        if($notify_url == 'native'){
            $trade_type = 'NATIVE';
        }else{
            $trade_type = 'JSAPI';
        }
        // 当前时间
        $time = time();
        // 生成随机字符串
        $nonceStr = md5($time . $openid);
		// API参数
		$params = [
			'attach' => $attach,
			'nonce_str' => $nonceStr,//随机字符串
			'body' => $attach,//商品描述
			'out_trade_no' => $order_no,//商户订单号
			'total_fee' => $total_fee * 100, // 价格:单位分
			'spbill_create_ip' => \request()->ip(),//服务终端IP
			'notify_url' => base_url() . 'task/notify/' . $notify_url,  // 异步通知地址
			'trade_type' => $trade_type,//交易类型	
		];
		if($this->config['is_sub'] == 1){
            //服务商统一下单
            $values = Setting::getItem('wxpayisp',0);
			$this->config['api_key'] = $values['api_key'];//服务商商户的密钥
			$params['appid'] = $values['app_id'];//服务商商户的APPID
			$params['mch_id'] = $values['mch_id'];//服务商商户号
			$params['sub_appid'] = $this->config['app_id'];//当前调起支付的小程序APPID
			$params['sub_mch_id'] = $this->config['mch_id'];//服务商分配的子商户号
			$params['sub_openid'] = $openid;//下单用户标识
			
		}else{
			$params['appid'] = $this->config['app_id'];//小程序ID
			$params['mch_id'] = $this->config['mch_id'];//商户号
			$params['openid'] = $openid;//下单用户标识
		}
        if($trade_type == 'JSAPI' AND $this->config['profit_sharing'] == 1){
            $params['profit_sharing'] = 'Y';//开启分账
        }
		
        // 生成签名
        $params['sign'] = $this->makeSign($params);
        // 请求API
        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        $result = $this->postXmlCurl($this->toXml($params), $url);
        $prepay = $this->fromXml($result);
        // 请求失败
        if ($prepay['return_code'] === 'FAIL') {
            die(json_encode(['code' => -10, 'msg' => $prepay['return_msg']]));
        }
        if ($prepay['result_code'] === 'FAIL') {
            die(json_encode(['code' => -10, 'msg' => $prepay['err_code_des']]));
        }
		//WEB扫码支付
		if(empty($openid) AND $trade_type=='NATIVE'){
			return $prepay['code_url'];//返回收款二维码 
		}
        // 生成 nonce_str 供前端使用
        $paySign = $this->makePaySign($params['nonce_str'], $prepay['prepay_id'], $time);
		//小程序支付
		return [
            'prepay_id' => $prepay['prepay_id'],
            'nonceStr' => $nonceStr,
            'timeStamp' => (string)$time,
            'paySign' => $paySign
        ];
    }

    /**
     * 支付成功异步通知
     */
    public function notify($Model,$method='edit',$api_key='')
    {
        //接收微信服务器回调的数据流
        if (!$xml = file_get_contents('php://input')) {
            $this->returnCode(false, 'Not found DATA');
        }
        // 将服务器返回的XML数据转化为数组
        $data = $this->fromXml($xml);
        // 订单信息
        $order = $Model->payDetail($data['out_trade_no']);
        empty($order) && $this->returnCode(true, '订单不存在');
        if(empty($api_key)){
            if(isset($data['sub_appid'])){
                $isp = Setting::getItem('wxpayisp',0);
                $this->config = $isp;//服务商商户的密钥
                $applet = Applet::getApplet(['app_id' => $data['sub_appid']]);
                $values = Setting::getItem('payment',$applet['applet_id']);
            }else{
                $applet = Applet::getApplet(['app_id' => $data['appid']]);
                $values = Setting::getItem('payment',$applet['applet_id']);
                $this->config = $values['wx'];
            }
            //判断是否分账
            if($values['wx']['profit_sharing'] == 1){
                $webpay = Setting::getItem('webpay',0);
                //判断站点是否开启支付分佣
                if($webpay['cost']['store_fee'] > 0){
                    // 当前时间
                    $time = time();
                    // 生成随机字符串
                    $nonceStr = md5($time . 'hema_self');
                    $params['nonce_str'] = $nonceStr;//随机字符串
                    $receiver = [
                            'type' => 'MERCHANT_ID',
                            'account' => $webpay['wx']['mch_id'],
                            'name' => $webpay['wx']['name'],
                            'relation_type' => 'SERVICE_PROVIDER'
                        ];
                    $params['receiver'] = json_encode($receiver);//分账接收方
                    //添加分账接收方
                    if(isset($data['sub_appid'])){
                        //服务商版
                        $params['appid'] = $isp['app_id'];//服务商公众号ID
                        $params['mch_id'] = $isp['mch_id'];//服务商商户号
                        $params['sub_mch_id'] = $values['wx']['mch_id'];//出资商户号
                        $params['sub_appid'] = $values['wx']['app_id'];
                    }else{
                        //普通版
                        $params['appid'] = $values['wx']['app_id'];//小程序ID
                        $params['mch_id'] = $values['wx']['mch_id'];//商户号
                    }
                    // 生成签名
                    $params['sign'] = $this->makeSign($params);
                    // 请求API
                    $url = 'https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver';
                    $result = $this->postXmlCurl($this->toXml($params), $url);
                    $prepay = $this->fromXml($result);
                    // 请求失败
                    if ($prepay['return_code'] != 'FAIL' AND $prepay['result_code'] != 'FAIL') {
                        //开始分账
                        $params['transaction_id'] = $data['transaction_id'];
                        $params['out_order_no'] = $data['out_trade_no'];
                        //计算分账金额
                        $amount = sprintf('%.2f',$order['pay_price'] * $webpay['cost']['store_fee'] / 100);
                        $receiver['amount'] = $amount;//分账金额(四舍五入保留两位小数)
                        $receiver['description'] = '分给服务商';//分账描述
                        // 生成签名
                        $params['sign'] = $this->makeSign($params);
                        // 请求API
                        $url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharing';
                        $result = $this->postXmlCurl($this->toXml($params), $url,true);
                        $prepay = $this->fromXml($result);
                        if ($prepay['return_code'] != 'FAIL' AND $prepay['result_code'] != 'FAIL') {
                            //分账成功
                            
                        }
                    }
                }
            }
        }else{
            $this->config['api_key'] = $api_key;
        }
        // 保存微信服务器返回的签名sign
        $dataSign = $data['sign'];
        // sign不参与签名算法
        unset($data['sign']);
        // 生成签名
        $sign = $this->makeSign($data);
        // 判断签名是否正确  判断支付状态
        if (($sign === $dataSign)
            && ($data['return_code'] == 'SUCCESS')
            && ($data['result_code'] == 'SUCCESS')) {
            if($method == 'add'){
                $Model->updatePayStatus($data['transaction_id'],$order);
                Cache::delete($data['out_trade_no']);
            }else{
                // 更新订单状态
                $order->updatePayStatus($data['transaction_id']);
            }
            // 返回状态
            $this->returnCode(true, 'OK');
        }
        // 返回状态
        $this->returnCode(false, '签名失败');
    }

    /**
     * 退款申请API
     */
    public function refund($refund_no,$transaction_id,$total_fee,$refund_fee,$refund_desc,$notify_url='orderRefund')
    {
        // 当前时间
        $time = time();
        // 生成随机字符串
        $nonceStr = md5($time);
        // API参数
        $params = [
            'nonce_str' => $nonceStr,//随机字符串
            'transaction_id' => $transaction_id,//微信支付订单号
            'out_refund_no' => $refund_no,//退款订单号
            'total_fee' => $total_fee * 100, // 订单总金额，价格:单位分
            'refund_fee' => $refund_fee * 100, // 退款总金额，价格:单位分
            'refund_desc' => $refund_desc,//退款原因
            'notify_url' => base_url() . 'task/notify/' . $notify_url  // 异步通知地址
        ];
        if($this->config['is_sub'] == 1){
            //服务商统一下单
            $values = Setting::getItem('wxpayisp',0);
            $this->config['api_key'] = $values['api_key'];//服务商商户的密钥
            $this->config['cert_pem'] = $values['cert_pem'];//证书
            $this->config['key_pem'] = $values['key_pem'];//证书密钥
            $params['appid'] = $values['app_id'];//服务商商户的APPID
            $params['mch_id'] = $values['mch_id'];//服务商商户号
            $params['sub_appid'] = $this->config['app_id'];//当前调起支付的小程序APPID
            $params['sub_mch_id'] = $this->config['mch_id'];//服务商分配的子商户号
            
        }else{
            $params['appid'] = $this->config['app_id'];//小程序ID
            $params['mch_id'] = $this->config['mch_id'];//商户号
        }
        //判断证书是否存在
        if(empty($this->config['cert_pem']) OR empty($this->config['key_pem'])){
            die(json_encode(['code' => 0, 'msg' => '未配置证书']));
        }
        // 生成签名
        $params['sign'] = $this->makeSign($params);
        // 请求API
        $url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
        $result = $this->postXmlCurl($this->toXml($params), $url,true);
        $prepay = $this->fromXml($result);
        // 请求失败
        if ($prepay['return_code'] === 'FAIL') {
            return $prepay['return_msg'];
        }
        if ($prepay['result_code'] === 'FAIL') {
            return $prepay['err_code_des'];
        }
        return false;
    }

    /**
    * 退款成功异步通知
    */
    public function notifyRefund($Model)
    {
        //接收微信服务器回调的数据流
        if (!$xml = file_get_contents('php://input')) {
            $this->returnCode(false, 'Not found DATA');
        }
        // 将服务器返回的XML数据转化为数组
        $data = $this->fromXml($xml);
        if($data['return_code'] != 'SUCCESS'){
            return false;
        }
        if(isset($data['sub_appid'])){
            $this->config = Setting::getItem('wxpayisp',0);
        }else{
            $applet = Applet::getApplet(['app_id' => $data['appid']]);
            $values = Setting::getItem('payment',$applet['applet_id']);
            $this->config = $values['wx'];
        }
        //解密数据
        $data = $this->refund_decrypt($data['req_info']);
        $data = $this->fromXml($data);
        // 订单信息
        $order = $Model->refundDetail($data['out_refund_no']);
        empty($order) && $this->returnCode(false, '订单不存在');
        if ($data['refund_status'] == 'SUCCESS') {
            // 更新订单状态
            $order->updateRefundStatus($data['refund_id']);
            // 返回状态
            $this->returnCode(true, '退款成功');
        }
        // 返回状态
        $this->returnCode(false, '退款失败');
    }

    /**
     * 企业转账到零钱API
     */
    public function transfers($open_id,$order_no,$price,$desc='用户提现')
    {
        // 生成随机字符串
        $nonceStr = md5(time());
        // API参数
        $params = [
            'openid' => $open_id,//用户openid
            'partner_trade_no' => $order_no, // 商户订单号
            'amount' => $price * 100, // 转账金额
            'mch_appid' => $this->config['app_id'],//商户账号appid    
            'mchid' => $this->config['mchid'],//商户号
            'check_name' => 'NO_CHECK',  // 校验用户姓名 NO_CHECK：不校验真实姓名 FORCE_CHECK：强校验真实姓名
            'nonce_str' => $nonceStr,//随机字符串
            'desc' => $desc
        ];
        // 生成签名
        $params['sign'] = $this->makeSign($params);
        // 请求API
        $url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
        $result = $this->postXmlCurl($this->toXml($params), $url,true);
        $prepay = $this->fromXml($result);
        // 请求失败
        if ($prepay['return_code'] === 'FAIL') {
            return [
                'code' => 1,
                'msg' => $prepay['return_msg']
            ];
        }
        if ($prepay['result_code'] === 'FAIL') {
            return [
                'code' => 1,
                'msg' => $prepay['err_code_des']
            ];
        }
        return [
            'code' => 0,
            'msg' => 'ok',
            'data' => $prepay
        ];
    }

    


    /*
     * 退款通知解密
     */
    private function refund_decrypt($req_info) 
    {
        $key = strtolower(md5($this->config['api_key']));
        return openssl_decrypt($req_info, "AES-256-ECB", $key);
    }

    /**
     * 返回状态给微信服务器
     */
    private function returnCode($is_success = true, $msg = null)
    {
        $xml_post = $this->toXml([
            'return_code' => $is_success ? 'SUCCESS' : 'FAIL',
            'return_msg' => $is_success ? 'OK' : $msg,
        ]);
        die($xml_post);
    }

    /**
     * 生成paySign
     */
    private function makePaySign($nonceStr, $prepay_id, $timeStamp)
    {
        $data = [
            'appId' => $this->config['app_id'],
            'nonceStr' => $nonceStr,
            'package' => 'prepay_id=' . $prepay_id,
            'signType' => 'MD5',
            'timeStamp' => $timeStamp,
        ];
        //签名步骤一：按字典序排序参数
        ksort($data);
        $string = $this->toUrlParams($data);
        //签名步骤二：在string后加入KEY
        $string = $string . '&key=' . $this->config['api_key'];
        //签名步骤三：MD5加密
        $string = md5($string);
        //签名步骤四：所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    /**
     * 将xml转为array
     */
    private function fromXml($xml)
    {
        // 禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
    }

    /**
     * 以post方式提交xml到对应的接口url
     */
    private function postXmlCurl($xml, $url, $cert = false, $second = 30)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);// 设置超时时间
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);//https请求 不验证证书和host
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);//严格校验
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);// 要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_POST, TRUE);// post提交方式
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);// 是否返回请求头
        //判断是否使用证书
        if($cert){
            $path = root_path() . '/extend/hema/wechat/cert/';
            file_put_contents($path . 'apiclient_cert.pem',$this->config['cert_pem']);
            file_put_contents($path . 'apiclient_key.pem',$this->config['key_pem']);
            curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
            curl_setopt($ch,CURLOPT_SSLCERT,$path . 'apiclient_cert.pem');
            curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
            curl_setopt($ch,CURLOPT_SSLKEY,$path . 'apiclient_key.pem');
        }
        $data = curl_exec($ch);// 运行curl
        curl_close($ch);
        return $data;
    }

    /**
     * 生成签名
     */
    private function makeSign($values)
    {
        //签名步骤一：按字典序排序参数
        ksort($values);
        $string = $this->toUrlParams($values);
        //签名步骤二：在string后加入KEY
        $string = $string . '&key=' . $this->config['api_key'];
        //签名步骤三：MD5加密
        $string = md5($string);
        //签名步骤四：所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    /**
     * 格式化参数格式化成url参数
     */
    private function toUrlParams($values)
    {
        $buff = '';
        foreach ($values as $k => $v) {
            if ($k != 'sign' && $v != '' && !is_array($v)) {
                $buff .= $k . '=' . $v . '&';
            }
        }
        return trim($buff, '&');
    }

    /**
     * 输出xml字符
     */
    private function toXml($values)
    {
        if (!is_array($values)
            || count($values) <= 0
        ) {
            return false;
        }

        $xml = "<xml>";
        foreach ($values as $key => $val) {
            if (is_numeric($val)) {
                $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
            } else {
                $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
            }
        }
        $xml .= "</xml>";
        return $xml;
    }

}
